package cmd

import (
	"bufio"
	"bytes"
	"cke.io/ckectl/app/apis/v1alpha1"
	"cke.io/ckectl/app/util"
	"fmt"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	flag "github.com/spf13/pflag"
	"gopkg.in/yaml.v2"
	"io"
	"io/ioutil"
	corev1 "k8s.io/api/core/v1"
	kuberuntime "k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	utilyaml "k8s.io/apimachinery/pkg/util/yaml"
	"path/filepath"
	"reflect"
	"strings"
)

type GVKContent struct {
	GVK     schema.GroupVersionKind
	Content []byte
}

type GvkSlice []GVKContent

var (
	scheme = kuberuntime.NewScheme()
	codec  kuberuntime.Codec
)

func init() {
	_ = v1alpha1.AddToScheme(scheme)          // registry
	_ = corev1.AddToScheme(scheme)            // corev1
	codec = GetCkeadmDefaultCodec(scheme) // codec
}

func NewCmdTemplate() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "template",
		Short: "",
		Run:   TemplateProcess,
		Args:  cobra.NoArgs,

		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			return nil
		},
	}
	AddTemplateFlags(cmd.Flags())
	return cmd
}

func TemplateProcess(cmd *cobra.Command, args []string) {
	ckeComponents, ckeCluster, ckeNodes, err := GetComponents(templateOptions.filePath)
	if err != nil {
		logrus.Errorf("failed to read CR from files, error: %s", err.Error())
		return
	}

	err = util.ProcessTemplate(ckeCluster, (*ckeComponents)[0], *ckeNodes, (*ckeNodes)[0], templateOptions.templatePath, templateOptions.key)
	if err != nil {
		logrus.Errorf("process template failed: %s", err.Error())
		return
	}

	logrus.Info("--- ckectl template process completed ---")
}

type TemplateOptions struct {
	filePath     string
	templatePath string
	key          string
}

var templateOptions = &TemplateOptions{}

func AddTemplateFlags(flagSet *flag.FlagSet) {
	flagSet.StringVar(
		&templateOptions.filePath, "file", "",
		"ckeCluster ckeNode ckeComponent CR yaml file path or directory path",
	)
	flagSet.StringVar(
		&templateOptions.templatePath, "template", "",
		`script template file path or directory path`,
	)
	flagSet.StringVar(
		&templateOptions.key, "key", "",
		`base64 encoded key`,
	)
}

func GetCkeadmDefaultCodec(scheme *kuberuntime.Scheme) kuberuntime.Codec {
	var codecfactory = serializer.NewCodecFactory(scheme)
	var infoer, _ = kuberuntime.SerializerInfoForMediaType(codecfactory.SupportedMediaTypes(), kuberuntime.ContentTypeYAML)

	return NewCkeadmCodec(infoer.Serializer, codecfactory.UniversalDecoder())
}

type ckeadmcodec struct {
	kuberuntime.Encoder
	kuberuntime.Decoder
}

func NewCkeadmCodec(encoder kuberuntime.Encoder, decoder kuberuntime.Decoder) kuberuntime.Codec {
	return &ckeadmcodec{encoder, decoder}
}

//get CkeCluster CkeNode CkeComponent
func GetComponents(path string) (*[]v1alpha1.CkeComponent, v1alpha1.CkeCluster, *[]v1alpha1.CkeNode, error) {
	//存储用户的ckeNode ckeCluster ckeComponent
	var ckeCluster v1alpha1.CkeCluster
	var ckeNodes = new([]v1alpha1.CkeNode)
	var userComponents = new([]v1alpha1.CkeComponent)
	err := GetCkeClusterFromFile(path, &ckeCluster)
	if err != nil {
		logrus.Errorf("failed to read ckeCluster from file, %s\n", err.Error())
		return nil, ckeCluster, nil, err
	}

	err = GetCkeNodeFromFile(path, ckeNodes)
	if err != nil {
		logrus.Errorf("failed to read ckeNodes from file, %s\n", err.Error())
		return nil, ckeCluster, nil, err
	}

	err = GetCkeComponentFromFile(path, userComponents)
	if err != nil {
		logrus.Errorf("failed to read user ckeComponents from file, %s\n", err.Error())
		return nil, ckeCluster, ckeNodes, err
	}

	return userComponents, ckeCluster, ckeNodes, nil
}

//read ckeNode from file
func GetCkeNodeFromFile(path string, nodes *[]v1alpha1.CkeNode) (err error) {
	var context []byte
	var gvkSlice = new(GvkSlice)
	var files []string

	if files, err = util.ListAllFilesInPath(path); err != nil {
		return err
	}

	// read files to gvk content slice
	for _, file := range files {
		if context, err = ioutil.ReadFile(filepath.Clean(file)); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
	}

	return DocumentSliceToNode(gvkSlice, nodes)
}

//decoded into ckeNode
func DocumentSliceToNode(gvkslice *GvkSlice, nodes *[]v1alpha1.CkeNode) error {
	if err := DocumentListToObjects(gvkslice); err != nil {
		return err
	}

	for _, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			return fmt.Errorf("no kind %s is registered for version %s", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *v1alpha1.CkeCluster:
			//do nothing
		case *v1alpha1.CkeNode:
			var ckenode v1alpha1.CkeNode
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, &ckenode); err != nil {
				return err
			}
			*nodes = append(*nodes, ckenode)
		case *v1alpha1.CkeComponent:
			//do nothing
		default:
			logrus.Warningf("kind %s version %s configuration found, but ckeadm unsupported", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
	}
	return nil
}

//read ckeCluster from file
func GetCkeClusterFromFile(path string, cluster *v1alpha1.CkeCluster) (err error) {
	var context []byte
	var gvkSlice = new(GvkSlice)
	var files []string

	if files, err = util.ListAllFilesInPath(path); err != nil {
		return err
	}

	// read files to gvk content slice
	for _, file := range files {
		if context, err = ioutil.ReadFile(filepath.Clean(file)); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
	}

	return DocumentSliceToCluster(gvkSlice, cluster)
}

//decoded into ckeCluster
func DocumentSliceToCluster(gvkslice *GvkSlice, cluster *v1alpha1.CkeCluster) error {
	var clusterInit bool

	if err := DocumentListToObjects(gvkslice); err != nil {
		return err
	}

	for _, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			return fmt.Errorf("no kind %s is registered for version %s", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *v1alpha1.CkeCluster:
			if clusterInit {
				return fmt.Errorf("has more than one cluster cr information")
			}
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, cluster); err != nil {
				return err
			}
			clusterInit = true
		case *v1alpha1.CkeNode:
			//do nothing
		case *v1alpha1.CkeComponent:
			//do nothing
		default:
			logrus.Warningf("kind %s version %s configuration found, but ckeadm unsupported", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
	}

	if clusterInit == false {
		return fmt.Errorf("can't find cluster cr information")
	}
	return nil
}

// SplitYAMLDocuments reads the YAML bytes per-document, unmarshals the TypeMeta information from each document
// and returns a map between the GroupVersionKind of the document and the document bytes
func SplitYAMLDocuments(yamls []byte, gvkSlice *GvkSlice) error {
	reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewBuffer(yamls)))
	for {
		typeMetaInfo := kuberuntime.TypeMeta{}
		// Read one YAML document at a time, until io.EOF is returned
		b, err := reader.Read()
		if err == io.EOF {
			break
		} else if err != nil {
			return err
		}

		if len(strings.ReplaceAll(string(b), "\n", "")) == 0 {
			continue
		}

		// Deserialize the TypeMeta information of this byte slice
		if err := yaml.Unmarshal(b, &typeMetaInfo); err != nil {
			return err
		}

		// Require TypeMeta information to be present
		if len(typeMetaInfo.APIVersion) == 0 || len(typeMetaInfo.Kind) == 0 {
			return errors.New("invalid configuration: kind and apiVersion is mandatory information that needs to be specified in all YAML documents")
		}

		// Build a GroupVersionKind object from the deserialized TypeMeta object
		gv, err := schema.ParseGroupVersion(typeMetaInfo.APIVersion)
		if err != nil {
			return errors.Wrap(err, "unable to parse apiVersion")
		}
		gvk := gv.WithKind(typeMetaInfo.Kind)

		// Save the mapping between the gvk and the bytes that object consists of
		*gvkSlice = append(*gvkSlice, GVKContent{GVK: gvk, Content: b})
	}

	return nil
}

//read ckeComponent from file
func GetCkeComponentFromFile(path string, components *[]v1alpha1.CkeComponent) (err error) {
	var context []byte
	var gvkSlice = new(GvkSlice)
	var files []string

	if files, err = util.ListAllFilesInPath(path); err != nil {
		return err
	}

	// read files to gvk content slice
	for _, file := range files {
		if context, err = ioutil.ReadFile(filepath.Clean(file)); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
	}

	return DocumentSliceToComponent(gvkSlice, components)
}

// Decode component yaml into ckeComponent
func DocumentSliceToComponent(gvkslice *GvkSlice, components *[]v1alpha1.CkeComponent) error {
	if err := DocumentListToObjects(gvkslice); err != nil {
		return err
	}

	for _, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			return fmt.Errorf("no kind %s is registered for version %s", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *v1alpha1.CkeCluster:
			//do nothing
		case *v1alpha1.CkeNode:
			//do nothing
		case *v1alpha1.CkeComponent:
			var ckeComponent v1alpha1.CkeComponent
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, &ckeComponent); err != nil {
				return err
			}
			*components = append(*components, ckeComponent)
		default:
			logrus.Warningf("kind %s version %s configuration found, but ckeadm unsupported", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
	}
	return nil
}

// issue: support v1 list. so you can use follow steps get v3 configuration
// ```
// # mkdir -p /root/cke-config/
// # kubectl get ckenode -A -o yaml > /root/cke-config/node.yml
// # kubectl get ckecluster -A -o yaml > /root/cke-config/cluster.yml
// # ckeadm transform -f config
// ```
func DocumentListToObjects(gvkslice *GvkSlice) error {
	for item, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			continue
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *corev1.List:
			var list = new(corev1.List)
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, list); err != nil {
				return err
			}
			*gvkslice = append((*gvkslice)[:item], (*gvkslice)[item+1:]...)
			for _, object := range list.Items {
				if err := SplitYAMLDocuments(object.Raw, gvkslice); err != nil {
					return err
				}
			}
			return DocumentListToObjects(gvkslice)
		}
	}

	return nil
}

func GetCkeClusterFromYaml(yaml []byte, cluster *v1alpha1.CkeCluster) (err error) {
	var gvkSlice = new(GvkSlice)

	if err = SplitYAMLDocuments(yaml, gvkSlice); err != nil {
		return fmt.Errorf("SplitYamlFailed: %s", err.Error())
	}

	return DocumentSliceToCluster(gvkSlice, cluster)
}

func GetCkeNodeFromYaml(yaml []byte, nodes *[]v1alpha1.CkeNode) (err error) {
	var gvkSlice = new(GvkSlice)

	if err = SplitYAMLDocuments(yaml, gvkSlice); err != nil {
		return fmt.Errorf("SplitYamlFailed: %s", err.Error())
	}

	return DocumentSliceToNode(gvkSlice, nodes)
}

func GetCkeComponentFromYaml(yaml []byte, components *[]v1alpha1.CkeComponent) (err error) {
	var gvkSlice = new(GvkSlice)

	if err = SplitYAMLDocuments(yaml, gvkSlice); err != nil {
		return fmt.Errorf("SplitYamlFailed: %s", err.Error())
	}

	return DocumentSliceToComponent(gvkSlice, components)
}

func GetSingleCkeComponentFromYaml(yaml []byte, component *v1alpha1.CkeComponent) (err error) {
	var gvkSlice = new(GvkSlice)

	if err = SplitYAMLDocuments(yaml, gvkSlice); err != nil {
		return fmt.Errorf("SplitYamlFailed: %s", err.Error())
	}

	return DocumentSliceToSingleComponent(gvkSlice, component)
}

// Decode component yaml into ckeComponent
func DocumentSliceToSingleComponent(gvkslice *GvkSlice, component *v1alpha1.CkeComponent) error {
	if err := DocumentListToObjects(gvkslice); err != nil {
		return err
	}

	for _, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			return fmt.Errorf("no kind %s is registered for version %s", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *v1alpha1.CkeComponent:
			err := kuberuntime.DecodeInto(codec, gvkcontent.Content, component)
			if err != nil {
				return err
			}
		default:
			logrus.Warningf("kind %s version %s configuration found, but ckeadm unsupported", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
	}
	return nil
}
